package com.studerb.service;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.net.io.DotTerminatedMessageReader;
import org.apache.commons.net.nntp.NNTPCommand;
import org.apache.commons.net.nntp.NNTPReply;
import org.apache.log4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import com.studerb.dao.interfaces.IServerDao;
import com.studerb.events.ServerEvent;
import com.studerb.exceptions.ServiceException;
import com.studerb.model.Server;
import com.studerb.nntp.NntpConnection;
import com.studerb.service.interfaces.IServerService;

/**
 * Service class for crud and other operations on servers.<br/> Upon starting
 * the service, all operations that return the list of servers will at least
 * return a non-null empty list.<br/> Be careful when deleting servers as you
 * must update a new server to be the master server if deleting a server when
 * there are more than one (the server being deleted) servers remaining
 */
public class ServerService implements IServerService, ApplicationContextAware {
	private final Logger logger = Logger.getLogger(NewsGroupService.class);
	private final ReentrantLock masterServerLock = new ReentrantLock();
	private final Condition masterServerDefinedCondition = masterServerLock.newCondition();
	private boolean masterServerDefined = false;
	private List<Server> servers;
	private boolean started = false;
	private AbstractApplicationContext applicationContext;
	private TransactionTemplate transactionTemplate;

	@Autowired protected EventService eventService;
	@Autowired protected IServerDao serverDao;
	@Autowired protected PlatformTransactionManager transactionManager;

	@Transactional public synchronized void addServer(Server server) throws ServiceException {
		// make sure master server is set correctly
		if (servers.contains(server)) {
			throw new ServiceException("Server already added");
		}
		if (servers.isEmpty()) {
			server.setMaster(true);
			masterServerDefined = true;
		}

		else if (server.isMaster()) {
			masterServerDefined = true;
			for (Server s : servers) {
				if (s.isMaster()) {
					s.setMaster(false);
					serverDao.update(s);
					eventService.publish(new ServerEvent(s, ServerEvent.EventType.UPDATED));
					logger.debug("Unsetting master server status: " + s);
				}
			}
		}
		serverDao.create(server);
		servers.add(server);
		logger.info("Successfully added new server: " + server);
		eventService.publish(new ServerEvent(server, ServerEvent.EventType.ADDED));
		if (masterServerDefined) {
			signalMasterServer();
		}
	}

	public void awaitMasterServer() throws InterruptedException {
		masterServerLock.lock();
		try {
			while (!masterServerDefined) {
				masterServerDefinedCondition.await();
			}
		}
		finally {
			masterServerLock.unlock();
		}

	}

	@Transactional public void deleteServer(Server server) throws ServiceException {
		// check if were deleting the master server
		if (server.isMaster() && servers.size() > 1) {
			throw new ServiceException("Must assign another server as master before deleting current master server");
		}
		try {
			if (!servers.remove(server)) {
				throw new ServiceException("Trying to delete server that doesn't exist");
			}
			serverDao.delete(server);
			logger.info("Successfully deleted server: " + server.getHost());
		}
		catch (Exception e) {
			throw new ServiceException("Error deleting Server: ", e);
		}
		if (server.isMaster()) {
			masterServerDefined = false;
		}
		eventService.publish(ServerEvent.class, new ServerEvent(server, ServerEvent.EventType.DELETED));
	}

	public Server getMasterServer() {
		for (Server s : servers) {
			if (s.isMaster()) {
				return s;
			}
		}
		return null;
	}

	public synchronized int getMaxConnections() {
		int maxConnections = 0;
		for (Server s : servers) {
			maxConnections += s.getConnections();
		}
		return maxConnections;
	}

	public List<Server> getServers() {
		return Collections.unmodifiableList(servers);
	}

	public boolean isMasterServerDefined() {
		return masterServerDefined;
	}

	public boolean isStarted() {
		return started;
	}

	/**
	 * Refreshes the list of servers Should be called after any modification to
	 * any servers
	 * 
	 * @throws Exception
	 */
	private synchronized void refreshServers() {
		servers = serverDao.getAllByOrder("created", "asc");
		logger.debug("Refreshed servers: " + servers.size() + " servers in the database");
	}

	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		// TODO replace this with NNTPConnection factory
		this.applicationContext = (AbstractApplicationContext) applicationContext;
	}

	private void signalMasterServer() {
		masterServerLock.lock();
		try {
			masterServerDefined = true;
			logger.debug("Signalling to all: Master_Server_Defined");
			masterServerDefinedCondition.signalAll();
		}
		finally {
			masterServerLock.unlock();
		}
	}

	public void start() {
		if (started) {
			throw new RuntimeException("ServerService already Started");
		}
		transactionTemplate = new TransactionTemplate(transactionManager);

		transactionTemplate.execute(new TransactionCallbackWithoutResult() {
			@Override protected void doInTransactionWithoutResult(TransactionStatus arg0) {
				servers = serverDao.getAllByOrder("created", "asc");
			}
		});

		logger.info("**ServerService starting**");
		try {
			started = true;

			for (Server s : servers) {
				if (s.isMaster()) {
					masterServerDefined = true;
					signalMasterServer();
					break;
				}
			}
		}
		catch (Exception e) {
			throw new ServiceException("Error initializing serverService: " + ExceptionUtils.getRootCauseMessage(e), e);
		}
	}

	public void stop() {
		if (!started) {
			throw new RuntimeException("ServerService already Started");
		}
		logger.info("[**ServerService Stopping**]");
		started = false;
	}

	/**
	 * @param server
	 *            Server {@link com.studerb.model.Server Server} to test using a
	 *            basic command. No need for a connection pool
	 * @return True if the server can connect, authenticate if needed send a
	 *         help command, get the reply, and then disconnect false if any
	 *         exception is thrown, including timeout on socket connection
	 *         (meaning network is down)
	 */
	public boolean testServer(Server server) {
		if (server == null) {
			logger.debug("Failed - server is null");
			return false;
		}
		logger.debug("Test server: " + server.toString());
		NntpConnection conn = (NntpConnection) applicationContext.getBean("nntpConnection");
		conn.setServer(server);
		try {
			conn.connect();
			int ret = conn.sendCommand(NNTPCommand.HELP);
			if (ret != NNTPReply.HELP_TEXT_FOLLOWS) {
				throw new IOException("Bad response to Help Command: " + conn.getReplyCode() + " " + conn.getReplyString());
			}
			String helpString = IOUtils.toString(new DotTerminatedMessageReader(conn.getReader()));
			logger.debug("Help Text: " + helpString);
			server.setConnectedAlready(true);
			return true;
		}
		catch (IOException ioException) {
			return false;
		}
		finally {
			conn.disconnect();
		}
	}

	@Transactional public void updateServer(Server server) throws ServiceException {
		// check if were updating the master server and it's the only server
		Server masterServer;
		if (!servers.contains(server)) {
			throw new ServiceException("Trying to update non-existant server: " + server.toString());
		}
		// There are two conditions: the new server is set to master or not
		if (server.isMaster()) {
			// is the same master - just update other the server
			if (getMasterServer().equals(server)) {
				serverDao.update(server);
				eventService.publish(new ServerEvent(server, ServerEvent.EventType.UPDATED));
			}
			// server set to master so must unset old master
			else {
				masterServer = getMasterServer();
				masterServer.setMaster(false);
				serverDao.update(masterServer);
				eventService.publish(new ServerEvent(masterServer, ServerEvent.EventType.UPDATED));
				serverDao.update(server);
				eventService.publish(new ServerEvent(server, ServerEvent.EventType.UPDATED));
			}
		}
		// server is unset as master
		else {
			// server is current master
			if (getMasterServer().equals(server)) {
				throw new ServiceException("Must set another server as master instead of unsetting the current master");
			}
			else {
				serverDao.update(server);
				eventService.publish(new ServerEvent(server, ServerEvent.EventType.UPDATED));
			}
		}
		refreshServers();
		eventService.publish(ServerEvent.class, new ServerEvent(server, ServerEvent.EventType.UPDATED));
	}
}
